ActionViewController.swift (13949B)
1 // 2 // ActionViewController.swift 3 // highlighter action extension 4 // 5 // Created by Daniel DβAquino on 2024-08-09. 6 // 7 8 import UIKit 9 import MobileCoreServices 10 import UniformTypeIdentifiers 11 import SwiftUI 12 13 struct ShareExtensionView: View { 14 @State var highlighter_state: HighlighterState = .loading 15 let extensionContext: NSExtensionContext 16 @State var state: DamusState? = nil 17 @State var signedEvent: String? = nil 18 19 @State private var selectedText = "" 20 @State private var selectedTextHeight: CGFloat = .zero 21 @State private var selectedTextWidth: CGFloat = .zero 22 23 @Environment(\.scenePhase) var scenePhase 24 25 var body: some View { 26 VStack(spacing: 15) { 27 if let state { 28 switch self.highlighter_state { 29 case .loading: 30 ProgressView() 31 case .no_highlight_text: 32 Group { 33 Text("No text selected", comment: "Title indicating that a highlight cannot be posted because no text was selected.") 34 .font(.largeTitle) 35 .multilineTextAlignment(.center) 36 .padding() 37 Text("You cannot post a highlight because you have selected no text on the page! Please close this, select some text, and try again.", comment: "Label explaining a highlight cannot be made because there was no selected text, and some instructions on how to resolve the issue") 38 .multilineTextAlignment(.center) 39 Button(action: { 40 self.done() 41 }, label: { 42 Text("Close", comment: "Button label giving the user the option to close the sheet from which they were trying to post a highlight") 43 }) 44 .foregroundStyle(.secondary) 45 } 46 case .not_logged_in: 47 Group { 48 Text("Not logged in", comment: "Title indicating that a highlight cannot be posted because the user is not logged in.") 49 .font(.largeTitle) 50 .multilineTextAlignment(.center) 51 .padding() 52 Text("You cannot post a highlight because you are not logged in with a private key! Please close this, login with a private key (or nsec), and try again.", comment: "Label explaining a highlight cannot be made because the user is not logged in") 53 .multilineTextAlignment(.center) 54 Button(action: { 55 self.done() 56 }, label: { 57 Text("Close", comment: "Button label giving the user the option to close the sheet from which they were trying to post a highlight") 58 }) 59 .foregroundStyle(.secondary) 60 } 61 case .loaded(let highlighted_text, let source_url): 62 PostView( 63 action: .highlighting(HighlightContentDraft(selected_text: highlighted_text, source: .external_url(source_url))), 64 damus_state: state 65 ) 66 case .failed(let error): 67 Group { 68 Text("Error", comment: "Title indicating that an error has occurred.") 69 .font(.largeTitle) 70 .multilineTextAlignment(.center) 71 .padding() 72 Text("An unexpected error occurred. Please contact Damus support via [Nostr](damus:npub18m76awca3y37hkvuneavuw6pjj4525fw90necxmadrvjg0sdy6qsngq955) or [email](support@damus.io) with the error message below.", comment: "Label explaining there was an error, and suggesting next steps") 73 .multilineTextAlignment(.center) 74 Text("Error: \(error)") 75 Button(action: { 76 self.done() 77 }, label: { 78 Text("Close", comment: "Button label giving the user the option to close the sheet from which they were trying to post a highlight") 79 }) 80 .foregroundStyle(.secondary) 81 } 82 case .posted(event: let event): 83 Group { 84 Image(systemName: "checkmark.circle.fill") 85 .resizable() 86 .frame(width: 60, height: 60) 87 Text("Posted", comment: "Title indicating that the user has posted a highlight successfully") 88 .font(.largeTitle) 89 .multilineTextAlignment(.center) 90 .padding(.bottom) 91 92 Link(destination: URL(string: "damus:\(event.id.bech32)")!, label: { 93 Text("Go to the app", comment: "Button label giving the user the option to go to the app after posting a highlight") 94 }) 95 .buttonStyle(GradientButtonStyle()) 96 Button(action: { 97 self.done() 98 }, label: { 99 Text("Close", comment: "Button label giving the user the option to close the sheet from which they posted a highlight") 100 }) 101 .foregroundStyle(.secondary) 102 } 103 case .cancelled: 104 Group { 105 Text("Cancelled", comment: "Title indicating that the user has cancelled.") 106 .font(.largeTitle) 107 .padding() 108 Button(action: { 109 self.done() 110 }, label: { 111 Text("Close", comment: "Button label giving the user the option to close the sheet from which they were trying to post a highlight") 112 }) 113 .foregroundStyle(.secondary) 114 } 115 case .posting: 116 Group { 117 ProgressView() 118 .frame(width: 20, height: 20) 119 Text("Posting", comment: "Title indicating that the highlight post is being published to the network") 120 .font(.largeTitle) 121 .multilineTextAlignment(.center) 122 .padding(.bottom) 123 Text("Your highlight is being broadcasted to the network. Please wait.", comment: "Label explaining there their highlight publishing action is in progress") 124 .multilineTextAlignment(.center) 125 .padding() 126 } 127 } 128 } 129 } 130 .onAppear(perform: { 131 self.loadSharedUrl() 132 guard let keypair = get_saved_keypair() else { return } 133 guard keypair.privkey != nil else { 134 self.highlighter_state = .not_logged_in 135 return 136 } 137 self.state = DamusState(keypair: keypair) 138 }) 139 .onChange(of: self.highlighter_state) { 140 if case .cancelled = highlighter_state { 141 self.done() 142 } 143 } 144 .onReceive(handle_notify(.post)) { post_notification in 145 switch post_notification { 146 case .post(let post): 147 self.post(post) 148 case .cancel: 149 self.highlighter_state = .cancelled 150 } 151 } 152 .onChange(of: scenePhase) { (phase: ScenePhase) in 153 guard let state else { return } 154 switch phase { 155 case .background: 156 print("txn: π HIGHLIGHTER BACKGROUNDED") 157 Task { @MainActor in 158 state.ndb.close() 159 } 160 break 161 case .inactive: 162 print("txn: π HIGHLIGHTER INACTIVE") 163 break 164 case .active: 165 print("txn: π HIGHLIGHTER ACTIVE") 166 state.pool.ping() 167 @unknown default: 168 break 169 } 170 } 171 .onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { obj in 172 guard let state else { return } 173 print("txn: π HIGHLIGHTER ACTIVE NOTIFY") 174 if state.ndb.reopen() { 175 print("txn: HIGHLIGHTER NOSTRDB REOPENED") 176 } else { 177 print("txn: HIGHLIGHTER NOSTRDB FAILED TO REOPEN closed: \(state.ndb.is_closed)") 178 } 179 } 180 .onReceive(NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification)) { obj in 181 guard let state else { return } 182 print("txn: π HIGHLIGHTER BACKGROUNDED") 183 Task { @MainActor in 184 state.ndb.close() 185 } 186 } 187 } 188 189 func loadSharedUrl() { 190 guard 191 let extensionItem = extensionContext.inputItems.first as? NSExtensionItem, 192 let itemProvider = extensionItem.attachments?.first else { 193 self.highlighter_state = .failed(error: "Can't get itemProvider") 194 return 195 } 196 197 let propertyList = UTType.propertyList.identifier 198 if itemProvider.hasItemConformingToTypeIdentifier(propertyList) { 199 itemProvider.loadItem(forTypeIdentifier: propertyList, options: nil, completionHandler: { (item, error) -> Void in 200 guard let dictionary = item as? NSDictionary else { return } 201 if error != nil { 202 self.highlighter_state = .failed(error: "Error loading plist item: \(error?.localizedDescription ?? "Unknown")") 203 return 204 } 205 OperationQueue.main.addOperation { 206 if let results = dictionary[NSExtensionJavaScriptPreprocessingResultsKey] as? NSDictionary, 207 let urlString = results["URL"] as? String, 208 let selection = results["selectedText"] as? String, 209 let url = URL(string: urlString) { 210 guard selection != "" else { 211 self.highlighter_state = .no_highlight_text 212 return 213 } 214 self.highlighter_state = .loaded(highlighted_text: selection, source_url: url) 215 } 216 else { 217 self.highlighter_state = .failed(error: "Cannot load results") 218 } 219 } 220 }) 221 } 222 else { 223 self.highlighter_state = .failed(error: "No plist detected") 224 } 225 } 226 227 func post(_ post: NostrPost) { 228 self.highlighter_state = .posting 229 guard let state else { 230 self.highlighter_state = .failed(error: "Damus state not initialized") 231 return 232 } 233 guard let full_keypair = state.keypair.to_full() else { 234 self.highlighter_state = .not_logged_in 235 return 236 } 237 guard let posted_event = post.to_event(keypair: full_keypair) else { 238 self.highlighter_state = .failed(error: "Cannot convert post data into a nostr event") 239 return 240 } 241 state.postbox.send(posted_event, on_flush: .once({ flushed_event in 242 if flushed_event.event.id == posted_event.id { 243 DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: { // Offset labor perception bias 244 self.highlighter_state = .posted(event: flushed_event.event) 245 }) 246 } 247 else { 248 self.highlighter_state = .failed(error: "Flushed event is not the event we just tried to post.") 249 } 250 })) 251 } 252 253 func done() { 254 self.extensionContext.completeRequest(returningItems: [], completionHandler: nil) 255 } 256 257 enum HighlighterState: Equatable { 258 case loading 259 case no_highlight_text 260 case not_logged_in 261 case loaded(highlighted_text: String, source_url: URL) 262 case posting 263 case posted(event: NostrEvent) 264 case cancelled 265 case failed(error: String) 266 } 267 } 268 269 class ActionViewController: UIViewController { 270 override func viewDidLoad() { 271 super.viewDidLoad() 272 self.view.tintColor = UIColor(DamusColors.purple) 273 274 DispatchQueue.main.async { 275 let contentView = UIHostingController(rootView: ShareExtensionView(extensionContext: self.extensionContext!)) 276 self.addChild(contentView) 277 self.view.addSubview(contentView.view) 278 279 // set up constraints 280 contentView.view.translatesAutoresizingMaskIntoConstraints = false 281 contentView.view.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true 282 contentView.view.bottomAnchor.constraint (equalTo: self.view.bottomAnchor).isActive = true 283 contentView.view.leftAnchor.constraint(equalTo: self.view.leftAnchor).isActive = true 284 contentView.view.rightAnchor.constraint (equalTo: self.view.rightAnchor).isActive = true 285 } 286 } 287 }